// @copy https://github.com/vercel/streamdown/blob/main/packages/streamdown/lib/parse-incomplete-markdown.ts
/* eslint-disable unicorn/prefer-string-slice */
const boldPattern = /(\*\*)([^*]*)$/
const italicPattern = /(__)([^_]*)$/
const boldItalicPattern = /(\*\*\*)([^*]*)$/
const singleAsteriskPattern = /(\*)([^*]*)$/
const singleUnderscorePattern = /(_)([^_]*)$/
const inlineCodePattern = /(`)([^`]*)$/
const strikethroughPattern = /(~~)([^~]*)$/
const whitespaceOrMarkersPattern = /^[\s_~*`]*$/
const listItemPattern = /^\s*[-*+]\s+$/
const letterNumberUnderscorePattern = /[\p{L}\p{N}_]/u
const inlineTripleBacktickPattern = /^```[^`\n]*```?$/
const fourOrMoreAsterisksPattern = /^\*{4,}$/

// OPTIMIZATION: Precompute which characters are word characters
// Using ASCII fast path before falling back to Unicode regex
const isWordChar = (char: string): boolean => {
  if (!char) {
    return false
  }
  // eslint-disable-next-line unicorn/prefer-code-point
  const code = char.charCodeAt(0)
  // ASCII optimization: a-z, A-Z, 0-9, _
  if (
    (code >= 48 && code <= 57) || // 0-9
    (code >= 65 && code <= 90) || // A-Z
    (code >= 97 && code <= 122) || // a-z
    code === 95 // _
  ) {
    return true
  }
  // Fallback to regex for Unicode characters (less common)
  return letterNumberUnderscorePattern.test(char)
}

// Helper function to check if we have a complete code block
const hasCompleteCodeBlock = (text: string): boolean => {
  const tripleBackticks = (text.match(/```/g) || []).length
  return tripleBackticks > 0 && tripleBackticks % 2 === 0 && text.includes("\n")
}

// Helper function to find the matching opening bracket for a closing bracket
// Handles nested brackets correctly by searching backwards
const findMatchingOpeningBracket = (text: string, closeIndex: number): number => {
  let depth = 1
  for (let i = closeIndex - 1; i >= 0; i -= 1) {
    if (text[i] === "]") {
      depth += 1
    } else if (text[i] === "[") {
      depth -= 1
      if (depth === 0) {
        return i
      }
    }
  }
  return -1 // No matching bracket found
}

// Helper function to find the matching closing bracket for an opening bracket
// Handles nested brackets correctly
const findMatchingClosingBracket = (text: string, openIndex: number): number => {
  let depth = 1
  for (let i = openIndex + 1; i < text.length; i += 1) {
    if (text[i] === "[") {
      depth += 1
    } else if (text[i] === "]") {
      depth -= 1
      if (depth === 0) {
        return i
      }
    }
  }
  return -1 // No matching bracket found
}

// Check if a position is inside a code block (between ``` or `)
const isInsideCodeBlock = (text: string, position: number): boolean => {
  // Check for inline code (backticks)
  let inInlineCode = false
  let inMultilineCode = false

  for (let i = 0; i < position; i += 1) {
    // Check for triple backticks (multiline code blocks)
    if (text.substring(i, i + 3) === "```") {
      inMultilineCode = !inMultilineCode
      i += 2 // Skip the next 2 backticks
      continue
    }

    // Only check for inline code if not in multiline code
    if (!inMultilineCode && text[i] === "`") {
      inInlineCode = !inInlineCode
    }
  }

  return inInlineCode || inMultilineCode
}

// Handles incomplete links and images by preserving them with a special marker
const handleIncompleteLinksAndImages = (text: string): string => {
  // Look for patterns like [text]( or ![text]( at the end of text
  // We need to handle nested brackets in the link text

  // Start from the end and look for ]( pattern
  const lastParenIndex = text.lastIndexOf("](")
  if (lastParenIndex !== -1 && !isInsideCodeBlock(text, lastParenIndex)) {
    // Check if this ]( is not followed by a closing )
    const afterParen = text.substring(lastParenIndex + 2)
    if (!afterParen.includes(")")) {
      // We have an incomplete URL like [text](partial-url
      // Now find the matching opening bracket for the ] before (
      const openBracketIndex = findMatchingOpeningBracket(text, lastParenIndex)

      if (openBracketIndex !== -1 && !isInsideCodeBlock(text, openBracketIndex)) {
        // Check if there's a ! before the [
        const isImage = openBracketIndex > 0 && text[openBracketIndex - 1] === "!"
        const startIndex = isImage ? openBracketIndex - 1 : openBracketIndex

        // Extract everything before this link/image
        const beforeLink = text.substring(0, startIndex)
        const linkText = text.substring(openBracketIndex + 1, lastParenIndex)

        if (isImage) {
          // For images with incomplete URLs, remove them entirely
          return beforeLink
        }

        // For links with incomplete URLs, replace the URL with placeholder and close it
        return `${beforeLink}[${linkText}](streamdown:incomplete-link)`
      }
    }
  }

  // Then check for incomplete link text: [partial-text without closing ]
  // Search backwards for an opening bracket that doesn't have a matching closing bracket
  for (let i = text.length - 1; i >= 0; i -= 1) {
    if (text[i] === "[" && !isInsideCodeBlock(text, i)) {
      // Check if there's a ! before it
      const isImage = i > 0 && text[i - 1] === "!"
      const openIndex = isImage ? i - 1 : i

      // Check if we have a closing bracket after this
      const afterOpen = text.substring(i + 1)
      if (!afterOpen.includes("]")) {
        // This is an incomplete link/image
        const beforeLink = text.substring(0, openIndex)

        if (isImage) {
          // For images, we remove them as they can't show skeleton
          return beforeLink
        }

        // For links, preserve the text and close the link with a
        // special placeholder URL that indicates it's incomplete
        return `${text}](streamdown:incomplete-link)`
      }

      // If we found a closing bracket, we need to check if it's the matching one
      // (accounting for nested brackets)
      const closingIndex = findMatchingClosingBracket(text, i)
      if (closingIndex === -1) {
        // No matching closing bracket
        const beforeLink = text.substring(0, openIndex)

        if (isImage) {
          return beforeLink
        }

        return `${text}](streamdown:incomplete-link)`
      }
    }
  }

  return text
}

// Completes incomplete bold formatting (**)
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: "Complex markdown parsing logic with multiple edge cases"
const handleIncompleteBold = (text: string): string => {
  // Don't process if inside a complete code block
  if (hasCompleteCodeBlock(text)) {
    return text
  }

  const boldMatch = text.match(boldPattern)

  if (boldMatch) {
    // Don't close if there's no meaningful content after the opening markers
    // boldMatch[2] contains the content after **
    // Check if content is only whitespace or other emphasis markers
    const contentAfterMarker = boldMatch[2]
    if (!contentAfterMarker || whitespaceOrMarkersPattern.test(contentAfterMarker)) {
      return text
    }

    // Check if the bold marker is in a list item context
    // Find the position of the matched bold marker
    const markerIndex = text.lastIndexOf(boldMatch[1]!)
    const beforeMarker = text.substring(0, markerIndex)
    const lastNewlineBeforeMarker = beforeMarker.lastIndexOf("\n")
    const lineStart = lastNewlineBeforeMarker === -1 ? 0 : lastNewlineBeforeMarker + 1
    const lineBeforeMarker = text.substring(lineStart, markerIndex)

    // Check if this line is a list item with just the bold marker
    if (listItemPattern.test(lineBeforeMarker)) {
      // This is a list item with just emphasis markers
      // Check if content after marker spans multiple lines
      const hasNewlineInContent = contentAfterMarker.includes("\n")
      if (hasNewlineInContent) {
        // Don't complete if the content spans to another line
        return text
      }
    }

    const asteriskPairs = (text.match(/\*\*/g) || []).length
    if (asteriskPairs % 2 === 1) {
      return `${text}**`
    }
  }

  return text
}

// Completes incomplete italic formatting with double underscores (__)
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: "Complex markdown parsing logic with multiple edge cases"
const handleIncompleteDoubleUnderscoreItalic = (text: string): string => {
  const italicMatch = text.match(italicPattern)

  if (italicMatch) {
    // Don't close if there's no meaningful content after the opening markers
    // italicMatch[2] contains the content after __
    // Check if content is only whitespace or other emphasis markers
    const contentAfterMarker = italicMatch[2]
    if (!contentAfterMarker || whitespaceOrMarkersPattern.test(contentAfterMarker)) {
      return text
    }

    // Check if the underscore marker is in a list item context
    // Find the position of the matched underscore marker
    const markerIndex = text.lastIndexOf(italicMatch[1]!)
    const beforeMarker = text.substring(0, markerIndex)
    const lastNewlineBeforeMarker = beforeMarker.lastIndexOf("\n")
    const lineStart = lastNewlineBeforeMarker === -1 ? 0 : lastNewlineBeforeMarker + 1
    const lineBeforeMarker = text.substring(lineStart, markerIndex)

    // Check if this line is a list item with just the underscore marker
    if (listItemPattern.test(lineBeforeMarker)) {
      // This is a list item with just emphasis markers
      // Check if content after marker spans multiple lines
      const hasNewlineInContent = contentAfterMarker.includes("\n")
      if (hasNewlineInContent) {
        // Don't complete if the content spans to another line
        return text
      }
    }

    const underscorePairs = (text.match(/__/g) || []).length
    if (underscorePairs % 2 === 1) {
      return `${text}__`
    }
  }

  return text
}

// OPTIMIZATION: Counts single asterisks without split("").reduce()
// Counts single asterisks that are not part of double asterisks, not escaped, not list markers, and not word-internal
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: "Complex character counting logic with multiple edge cases"
const countSingleAsterisks = (text: string): number => {
  let count = 0
  const len = text.length

  for (let index = 0; index < len; index += 1) {
    if (text[index] !== "*") {
      continue
    }

    const prevChar = index > 0 ? text[index - 1] : ""
    const nextChar = index < len - 1 ? text[index + 1] : ""

    // Skip if escaped with backslash
    if (prevChar === "\\") {
      continue
    }

    // Skip if part of ** or ***
    if (prevChar === "*" || nextChar === "*") {
      continue
    }

    // Skip if asterisk is word-internal (between word characters)
    if (prevChar && nextChar && isWordChar(prevChar) && isWordChar(nextChar)) {
      continue
    }

    // Check if this is a list marker (asterisk at start of line followed by space)
    // Look backwards to find the start of the current line
    let lineStartIndex = 0
    for (let i = index - 1; i >= 0; i -= 1) {
      if (text[i] === "\n") {
        lineStartIndex = i + 1
        break
      }
    }

    // Check if this asterisk is at the beginning of a line (with optional whitespace)
    let isListMarker = true
    for (let i = lineStartIndex; i < index; i += 1) {
      if (text[i] !== " " && text[i] !== "\t") {
        isListMarker = false
        break
      }
    }

    if (isListMarker && (nextChar === " " || nextChar === "\t")) {
      continue
    }

    count += 1
  }

  return count
}

// Completes incomplete italic formatting with single asterisks (*)
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: "Complex italic handling logic with multiple edge cases for markdown parsing"
const handleIncompleteSingleAsteriskItalic = (text: string): string => {
  // Don't process if inside a complete code block
  if (hasCompleteCodeBlock(text)) {
    return text
  }

  const singleAsteriskMatch = text.match(singleAsteriskPattern)

  if (singleAsteriskMatch) {
    // Find the first single asterisk position (not part of ** and not word-internal)
    let firstSingleAsteriskIndex = -1
    for (let i = 0; i < text.length; i += 1) {
      if (text[i] === "*" && text[i - 1] !== "*" && text[i + 1] !== "*" && text[i - 1] !== "\\") {
        // Check if asterisk is word-internal (between word characters)
        const prevChar = i > 0 ? text[i - 1] : ""
        const nextChar = i < text.length - 1 ? text[i + 1] : ""
        if (prevChar && nextChar && isWordChar(prevChar) && isWordChar(nextChar)) {
          continue
        }

        firstSingleAsteriskIndex = i
        break
      }
    }

    if (firstSingleAsteriskIndex === -1) {
      return text
    }

    // Get content after the first single asterisk
    const contentAfterFirstAsterisk = text.substring(firstSingleAsteriskIndex + 1)

    // Check if there's meaningful content after the asterisk
    // Don't close if content is only whitespace or emphasis markers
    if (!contentAfterFirstAsterisk || whitespaceOrMarkersPattern.test(contentAfterFirstAsterisk)) {
      return text
    }

    const singleAsterisks = countSingleAsterisks(text)
    if (singleAsterisks % 2 === 1) {
      return `${text}*`
    }
  }

  return text
}

// Check if a position is within a math block (between $ or $$)
const isWithinMathBlock = (text: string, position: number): boolean => {
  // Count dollar signs before this position
  let inInlineMath = false
  let inBlockMath = false

  for (let i = 0; i < text.length && i < position; i += 1) {
    // Skip escaped dollar signs
    if (text[i] === "\\" && text[i + 1] === "$") {
      i += 1 // Skip the next character
      continue
    }

    if (text[i] === "$") {
      // Check for block math ($$)
      if (text[i + 1] === "$") {
        inBlockMath = !inBlockMath
        i += 1 // Skip the second $
        inInlineMath = false // Block math takes precedence
      } else if (!inBlockMath) {
        // Only toggle inline math if not in block math
        inInlineMath = !inInlineMath
      }
    }
  }

  return inInlineMath || inBlockMath
}

// OPTIMIZATION: Counts single underscores without split("").reduce()
// Counts single underscores that are not part of double underscores, not escaped, and not in math blocks
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: "Complex character counting logic with multiple edge cases"
const countSingleUnderscores = (text: string): number => {
  // OPTIMIZATION: For large texts, if there are no dollar signs, skip math block checking entirely
  const hasMathBlocks = text.includes("$")

  let count = 0
  const len = text.length

  for (let index = 0; index < len; index += 1) {
    if (text[index] !== "_") {
      continue
    }

    const prevChar = index > 0 ? text[index - 1] : ""
    const nextChar = index < len - 1 ? text[index + 1] : ""

    // Skip if escaped with backslash
    if (prevChar === "\\") {
      continue
    }

    // Skip if within math block (only check if text has dollar signs)
    if (hasMathBlocks && isWithinMathBlock(text, index)) {
      continue
    }

    // Skip if part of __
    if (prevChar === "_" || nextChar === "_") {
      continue
    }

    // Skip if underscore is word-internal (between word characters)
    if (prevChar && nextChar && isWordChar(prevChar) && isWordChar(nextChar)) {
      continue
    }

    count += 1
  }

  return count
}

// Completes incomplete italic formatting with single underscores (_)
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: "Complex italic handling logic with multiple edge cases for markdown parsing"
const handleIncompleteSingleUnderscoreItalic = (text: string): string => {
  // Don't process if inside a complete code block
  if (hasCompleteCodeBlock(text)) {
    return text
  }

  const singleUnderscoreMatch = text.match(singleUnderscorePattern)

  if (singleUnderscoreMatch) {
    // Find the first single underscore position (not part of __ and not word-internal)
    let firstSingleUnderscoreIndex = -1
    for (let i = 0; i < text.length; i += 1) {
      if (
        text[i] === "_" &&
        text[i - 1] !== "_" &&
        text[i + 1] !== "_" &&
        text[i - 1] !== "\\" &&
        !isWithinMathBlock(text, i)
      ) {
        // Check if underscore is word-internal (between word characters)
        const prevChar = i > 0 ? text[i - 1] : ""
        const nextChar = i < text.length - 1 ? text[i + 1] : ""
        if (prevChar && nextChar && isWordChar(prevChar) && isWordChar(nextChar)) {
          continue
        }

        firstSingleUnderscoreIndex = i
        break
      }
    }

    if (firstSingleUnderscoreIndex === -1) {
      return text
    }

    // Get content after the first single underscore
    const contentAfterFirstUnderscore = text.substring(firstSingleUnderscoreIndex + 1)

    // Check if there's meaningful content after the underscore
    // Don't close if content is only whitespace or emphasis markers
    if (
      !contentAfterFirstUnderscore ||
      whitespaceOrMarkersPattern.test(contentAfterFirstUnderscore)
    ) {
      return text
    }

    const singleUnderscores = countSingleUnderscores(text)
    if (singleUnderscores % 2 === 1) {
      // If text ends with newline(s), insert underscore before them
      // Use string methods instead of regex to avoid ReDoS vulnerability
      let endIndex = text.length
      while (endIndex > 0 && text[endIndex - 1] === "\n") {
        endIndex -= 1
      }
      if (endIndex < text.length) {
        const textBeforeNewlines = text.slice(0, endIndex)
        const trailingNewlines = text.slice(endIndex)
        return `${textBeforeNewlines}_${trailingNewlines}`
      }
      return `${text}_`
    }
  }

  return text
}

// Checks if a backtick at position i is part of a triple backtick sequence
const isPartOfTripleBacktick = (text: string, i: number): boolean => {
  const isTripleStart = text.substring(i, i + 3) === "```"
  const isTripleMiddle = i > 0 && text.substring(i - 1, i + 2) === "```"
  const isTripleEnd = i > 1 && text.substring(i - 2, i + 1) === "```"

  return isTripleStart || isTripleMiddle || isTripleEnd
}

// Counts single backticks that are not part of triple backticks
const countSingleBackticks = (text: string): number => {
  let count = 0
  for (let i = 0; i < text.length; i += 1) {
    if (text[i] === "`" && !isPartOfTripleBacktick(text, i)) {
      count += 1
    }
  }
  return count
}

// Completes incomplete inline code formatting (`)
// Avoids completing if inside an incomplete code block
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: "Complex inline code handling logic with multiple edge cases for markdown parsing"
const handleIncompleteInlineCode = (text: string): string => {
  // Check if we have inline triple backticks (starts with ``` and should end with ```)
  // This pattern should ONLY match truly inline code (no newlines)
  // Examples: ```code``` or ```python code```
  const inlineTripleBacktickMatch = text.match(inlineTripleBacktickPattern)
  if (inlineTripleBacktickMatch && !text.includes("\n")) {
    // Check if it ends with exactly 2 backticks (incomplete)
    if (text.endsWith("``") && !text.endsWith("```")) {
      return `${text}\``
    }
    // Already complete inline triple backticks
    return text
  }

  // Check if we're inside a multi-line code block (complete or incomplete)
  const allTripleBackticks = (text.match(/```/g) || []).length
  const insideIncompleteCodeBlock = allTripleBackticks % 2 === 1

  // Don't modify text if we have complete multi-line code blocks (even pairs of ```)
  if (allTripleBackticks > 0 && allTripleBackticks % 2 === 0 && text.includes("\n")) {
    // We have complete multi-line code blocks, don't add any backticks
    return text
  }

  // Special case: if text ends with ```\n (triple backticks followed by newline)
  // This is actually a complete code block, not incomplete
  if ((text.endsWith("```\n") || text.endsWith("```")) && allTripleBackticks % 2 === 0) {
    // Count all triple backticks - if even, it's complete
    return text
  }

  const inlineCodeMatch = text.match(inlineCodePattern)

  if (inlineCodeMatch && !insideIncompleteCodeBlock) {
    // Don't close if there's no meaningful content after the opening marker
    // inlineCodeMatch[2] contains the content after `
    // Check if content is only whitespace or other emphasis markers
    const contentAfterMarker = inlineCodeMatch[2]
    if (!contentAfterMarker || whitespaceOrMarkersPattern.test(contentAfterMarker)) {
      return text
    }

    const singleBacktickCount = countSingleBackticks(text)
    if (singleBacktickCount % 2 === 1) {
      return `${text}\``
    }
  }

  return text
}

// Completes incomplete strikethrough formatting (~~)
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: "Complex markdown parsing logic with multiple edge cases"
const handleIncompleteStrikethrough = (text: string): string => {
  const strikethroughMatch = text.match(strikethroughPattern)

  if (strikethroughMatch) {
    // Don't close if there's no meaningful content after the opening markers
    // strikethroughMatch[2] contains the content after ~~
    // Check if content is only whitespace or other emphasis markers
    const contentAfterMarker = strikethroughMatch[2]
    if (!contentAfterMarker || whitespaceOrMarkersPattern.test(contentAfterMarker)) {
      return text
    }

    const tildePairs = (text.match(/~~/g) || []).length
    if (tildePairs % 2 === 1) {
      return `${text}~~`
    }
  }

  return text
}

// Completes incomplete block KaTeX formatting ($$)
const handleIncompleteBlockKatex = (text: string): string => {
  // Count all $$ pairs in the text
  const dollarPairs = (text.match(/\$\$/g) || []).length

  // If we have an even number of $$, the block is complete
  if (dollarPairs % 2 === 0) {
    return text
  }

  // If we have an odd number, add closing $$
  // Check if this looks like a multi-line math block (contains newlines after opening $$)
  const firstDollarIndex = text.indexOf("$$")
  const hasNewlineAfterStart = firstDollarIndex !== -1 && text.includes("\n", firstDollarIndex)

  // For multi-line blocks, add newline before closing $$ if not present
  if (hasNewlineAfterStart && !text.endsWith("\n")) {
    return `${text}\n$$`
  }

  // For inline blocks or when already ending with newline, just add $$
  return `${text}$$`
}

// Counts triple asterisks that are not part of quadruple or more asterisks
// OPTIMIZATION: Count *** without regex to avoid allocation
const countTripleAsterisks = (text: string): number => {
  let count = 0
  let consecutiveAsterisks = 0

  // biome-ignore lint/style/useForOf: "Need index access to check character codes for performance"
  for (const element of text) {
    if (element === "*") {
      consecutiveAsterisks += 1
    } else {
      // End of asterisk sequence
      if (consecutiveAsterisks >= 3) {
        count += Math.floor(consecutiveAsterisks / 3)
      }
      consecutiveAsterisks = 0
    }
  }

  // Handle trailing asterisks
  if (consecutiveAsterisks >= 3) {
    count += Math.floor(consecutiveAsterisks / 3)
  }

  return count
}

// Completes incomplete bold-italic formatting (***)
const handleIncompleteBoldItalic = (text: string): string => {
  // Don't process if inside a complete code block
  if (hasCompleteCodeBlock(text)) {
    return text
  }

  // Don't process if text is only asterisks and has 4 or more consecutive asterisks
  // This prevents cases like **** from being treated as incomplete ***
  if (fourOrMoreAsterisksPattern.test(text)) {
    return text
  }

  const boldItalicMatch = text.match(boldItalicPattern)

  if (boldItalicMatch) {
    // Don't close if there's no meaningful content after the opening markers
    // boldItalicMatch[2] contains the content after ***
    // Check if content is only whitespace or other emphasis markers
    const contentAfterMarker = boldItalicMatch[2]
    if (!contentAfterMarker || whitespaceOrMarkersPattern.test(contentAfterMarker)) {
      return text
    }

    const tripleAsteriskCount = countTripleAsterisks(text)
    if (tripleAsteriskCount % 2 === 1) {
      return `${text}***`
    }
  }

  return text
}

// Parses markdown text and removes incomplete tokens to prevent partial rendering
export const parseIncompleteMarkdown = (text: string): string => {
  if (!text || typeof text !== "string") {
    return text
  }

  let result = text

  // Handle incomplete links and images first
  const processedResult = handleIncompleteLinksAndImages(result)

  // If we added an incomplete link marker, don't process other formatting
  // as the content inside the link should be preserved as-is
  if (processedResult.endsWith("](streamdown:incomplete-link)")) {
    return processedResult
  }

  result = processedResult

  // Handle various formatting completions
  // Handle triple asterisks first (most specific)
  result = handleIncompleteBoldItalic(result)
  result = handleIncompleteBold(result)
  result = handleIncompleteDoubleUnderscoreItalic(result)
  result = handleIncompleteSingleAsteriskItalic(result)
  result = handleIncompleteSingleUnderscoreItalic(result)
  result = handleIncompleteInlineCode(result)
  result = handleIncompleteStrikethrough(result)

  // Handle KaTeX formatting (only block math with $$)
  result = handleIncompleteBlockKatex(result)
  // Note: We don't handle inline KaTeX with single $ as they're likely currency symbols

  return result
}
